package net.aiion.weave.impl.injection;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import net.aiion.weave.spi.injection.InjectionContext;
import net.aiion.weave.spi.injection.InjectionException;
import net.aiion.weave.spi.injection.Injector;
import net.aiion.weave.spi.injection.annotations.Inject;
import net.aiion.weave.spi.naming.Context;

public class InjectionContextImpl implements InjectionContext {
	private static final Map<Class<?>, Class<?>> PRIMITIVE_TYPES;
	private static final int PRIMITIVE_TYPES_LENGTH = 9;

	static {
		PRIMITIVE_TYPES = new HashMap<>(PRIMITIVE_TYPES_LENGTH);
		PRIMITIVE_TYPES.put(void.class, Void.class);
		PRIMITIVE_TYPES.put(boolean.class, Boolean.class);
		PRIMITIVE_TYPES.put(char.class, Character.class);
		PRIMITIVE_TYPES.put(byte.class, Byte.class);
		PRIMITIVE_TYPES.put(short.class, Short.class);
		PRIMITIVE_TYPES.put(int.class, Integer.class);
		PRIMITIVE_TYPES.put(long.class, Long.class);
		PRIMITIVE_TYPES.put(float.class, Float.class);
		PRIMITIVE_TYPES.put(double.class, Double.class);
	}

	private final Context context;
	private final Iterator<Injector> injectorsIterator;
	private final Object injectable;

	private final String targetName;
	private final Class<?> targetType;
	private final Map<Class<? extends Annotation>, Annotation> targetAnnotations;

	public InjectionContextImpl(final Context aContext,
			final Collection<Injector> aInjectors, final Object aInjectable,
			final String aTargetName,
			final Class<?> aTargetType, final Annotation[] aTargetAnnotations) {
		context = aContext;
		injectorsIterator = aInjectors.iterator();
		injectable = aInjectable;
		targetName = aTargetName;
		if (PRIMITIVE_TYPES.containsKey(aTargetType)) {
			targetType = PRIMITIVE_TYPES.get(aTargetType);
		} else {
			targetType = aTargetType;
		}

		targetAnnotations = new HashMap<Class<? extends Annotation>, Annotation>(
				aTargetAnnotations.length);
		for (Annotation targetAnnotation : aTargetAnnotations) {
			targetAnnotations.put(targetAnnotation.annotationType(),
					targetAnnotation);
		}
	}

	@Override
	public Context getContext() {
		return context;
	}

	@Override
	public Object getInjectable() {
		return injectable;
	}

	@Override
	public Class<?> getTargetType() {
		return targetType;
	}

	@Override
	public Collection<? extends Annotation> getTargetAnnotations() {
		return targetAnnotations.values();
	}

	@Override
	public boolean hasTargetAnnotation(
			Class<? extends Annotation> aAnnotationType) {
		return targetAnnotations.containsKey(aAnnotationType);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T extends Annotation> T getTargetAnnotation(
			final Class<T> aAnnotationType) {
		return (T) targetAnnotations.get(aAnnotationType);
	}

	@Override
	public Object forwardInjection() {
		if (injectorsIterator.hasNext()) {
			return injectorsIterator.next().injectValue(this);
		}

		return lookupDefaultValue();
	}

	private Object lookupDefaultValue() {
		final Inject injectAnnotation = getTargetAnnotation(Inject.class);
		if (injectAnnotation == null) {
			throw new InjectionException(
					"Cannot inject into a non @Inject-annotated target.");
		}

		Object targetValue = lookupByContextualName(injectAnnotation.value());
		if (targetValue == null && targetName != null) {
			targetValue = lookupByContextualName(targetName);
		}

		if (targetValue == null) {
			targetValue = context.lookup(targetType);
		}

		if (targetValue == null) {
			throw new InjectionException(
					String.format(
							"Cannot inject value into target (tried @Inject-declared contextual name '%s', target name '%s' and target type name '%s')",
							injectAnnotation.value(), "", targetType.getName()));
		}
		
		return targetValue;
	}

	private Object lookupByContextualName(final String aContextualName) {
		if (aContextualName == null || "".equals(aContextualName)) {
			return null;
		}

		return context.lookup(targetType, aContextualName);
	}
}
